﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using grounding.nao.sensors;
using grounding.parser;
using grounding.parser.Parsers;
using System.Xml.Serialization;

namespace grounding.grounder
{
    [Serializable(), XmlRoot("TrainingData")]
    public class TrainingData : ISerializable
    {
        private const string SERIALIZATION_FOLDER = @"../../TrainingData/"; // Default = "../../TrainingData/"
        private const string SERIALIZATION_PREFIX = @"training_"; // Default = "training_"
        private const string SERIALIZATION_SUFFIX = @".dat"; // Default = ".dat"
        private const string SERIALIZATION_SUFFIX_XML = @".xml";

        private TrainingData() { }

        // The name of this training data set.
        [XmlAttribute("name")]
        private string name;

        // Category label.
        [XmlAttribute("category")]
        private int category;
        public int Category { get { return category; } }

        // The data.
        [XmlElement("data")]
        private List<double[]> dataVector;
        public double[][] DataVector { get { return dataVector.ToArray(); } }

        // The data labels.
        [XmlElement("classes")]
        private List<int> classes;
        public int[] Classes { get { return classes.ToArray(); } }

        #region Adding to TrainingData

        /// <summary>
        /// Adds an example to the existing set of training data.
        /// </summary>
        /// <param name="vector">the datavector to append</param>
        /// <param name="label">the label of <paramref name="vector"/></param>
        public void AddExample(double[] vector, int label)
        {
            dataVector.Add(vector);
            classes.Add(label);
        }

        /// <summary>
        /// Adds an example to the existing set of training data.
        /// </summary>
        /// <param name="vector">the datavector to append</param>
        /// <param name="label">the label of <paramref name="vector"/></param>
        public void AddExample(ISensorFrame frame, ILogicElement label)
        {
            dataVector.Add(frame.ToArray());
            classes.Add(((Label)label).NormalizedIdentifier);
        }

        /// <summary>
        /// Concatenates another TrainingData object with this one.
        /// </summary>
        /// <param name="other">other TrainingData object to concatenate with</param>
        public void Add(TrainingData other)
        {
            if (this.category != other.category)
                throw new ArgumentException("Can't concatenate two TrainingData objects with different categories");

            this.dataVector.AddRange(other.dataVector);
            this.classes.AddRange(other.classes);
        }

        #endregion

        #region Serialization

        public TrainingData(string name, int category) : this(name, category, new double[][] { }, new int[] { }) { }

        /// <summary>
        /// Creates a representation of the training data that is ready
        /// to be fed into the SVM.
        /// </summary>
        /// <param name="name">a name for this training data set</param>
        /// <param name="category">corresponds to enum category</param>
        /// <param name="dataVector">the data</param>
        /// <param name="classes">the label identifiers for the data</param>
        public TrainingData(string name, int category, double[][] dataVector, int[] classes)
        {
            this.name = name;
            this.category = category;
            this.dataVector = new List<double[]>();
            this.dataVector.AddRange(dataVector);
            this.classes = new List<int>();
            this.classes.AddRange(classes);
        }

        public TrainingData(string name, int category, ISensorFrame[] data, Label[] labels)
        {
            this.name = name;
            this.category = category;
            this.dataVector = new List<double[]>();
            foreach (ISensorFrame frame in data)
                this.dataVector.Add(frame.ToArray());
            this.classes = new List<int>();
            foreach (Label label in labels)
                this.classes.Add(label.NormalizedIdentifier);
        }

        /// <summary>
        /// Deserializes the object.
        /// </summary>
        /// <param name="info">the data necessary to deserialize this object</param>
        /// <param name="ctxt">specifies the serialization stream</param>
        public TrainingData(SerializationInfo info, StreamingContext ctxt)
        {
            name = (string)info.GetValue("name", typeof(string));
            category = (int)info.GetValue("category", typeof(int));
            dataVector = (List<double[]>)info.GetValue("dataVector", typeof(List<double[]>));
            classes = (List<int>)info.GetValue("classes", typeof(List<int>));
        }

        public void SaveToDisk() { SaveToDiskBinary(); }
        public void SaveToDiskXML()
        {
            // Ensure path exists.
            if (!Directory.Exists(SERIALIZATION_FOLDER))
                Directory.CreateDirectory(SERIALIZATION_FOLDER);

            XmlSerializer serializer = new XmlSerializer(typeof(TrainingData));
            TextWriter textWriter = new StreamWriter(SERIALIZATION_FOLDER + SERIALIZATION_PREFIX + name + SERIALIZATION_SUFFIX_XML);
            serializer.Serialize(textWriter, this);
            textWriter.Close();
        }

        /// <summary>
        /// Save this training data to disk.
        /// </summary>
        public void SaveToDiskBinary()
        {
            // Ensure path exists.
            if (!Directory.Exists(SERIALIZATION_FOLDER))
                Directory.CreateDirectory(SERIALIZATION_FOLDER);

            Stream stream = File.Open(SERIALIZATION_FOLDER + SERIALIZATION_PREFIX + name + SERIALIZATION_SUFFIX, FileMode.Create);
            BinaryFormatter bf = new BinaryFormatter();

            bf.Serialize(stream, this);
            stream.Close();
        }

        public static TrainingData LoadFromDisk(string name) { return LoadFromDiskBinary(name); }
        /// <summary>
        /// Loads training data from disk
        /// </summary>
        /// <param name="name">name of the training data set</param>
        /// <returns>The deserialized TrainingData object</returns>
        public static TrainingData LoadFromDiskXML(string name)
        {
            XmlSerializer deserializer = new XmlSerializer(typeof(TrainingData));
            TextReader textReader = new StreamReader(SERIALIZATION_FOLDER + SERIALIZATION_PREFIX + name + SERIALIZATION_SUFFIX_XML);
            TrainingData retVal = (TrainingData)deserializer.Deserialize(textReader);
            textReader.Close();

            return retVal;
        }

        /// <summary>
        /// Loads and returns the training data that was previously saved to
        /// disk under this name.
        /// </summary>
        /// <param name="name">the name this training data was saved under</param>
        /// <returns>the TrainingData object containing the loaded data</returns>
        public static TrainingData LoadFromDiskBinary(string name)
        {
            if (File.Exists(SERIALIZATION_FOLDER + SERIALIZATION_PREFIX + name + SERIALIZATION_SUFFIX))
            {
                Stream stream = File.Open(SERIALIZATION_FOLDER + SERIALIZATION_PREFIX + name + SERIALIZATION_SUFFIX, FileMode.Open);
                BinaryFormatter bf = new BinaryFormatter();

                // Deserialize the TrainingData into a new instance.
                TrainingData trainingData = (TrainingData)bf.Deserialize(stream);
                stream.Close();

                return trainingData;
            }
            return null;
        }

        /// <summary>
        /// Loads and returns the training data that was previously saved to
        /// disk under this name.
        /// </summary>
        /// <param name="name">the name this training data was saved under</param>
        /// <param name="category">corresponding to category enum</param>
        /// <param name="dataVector">the data</param>
        /// <param name="classes">the labels for this data</param>
        public static void LoadFromDiskBinary(string name, out int category, out double[][] dataVector, out int[] classes)
        {
            Stream stream = File.Open(SERIALIZATION_FOLDER + SERIALIZATION_PREFIX + name + SERIALIZATION_SUFFIX, FileMode.Open);
            BinaryFormatter bf = new BinaryFormatter();

            // Deserialize the TrainingData into a new instance.
            TrainingData trainingData = (TrainingData)bf.Deserialize(stream);
            stream.Close();

            // Output the data.
            category = trainingData.category;
            dataVector = trainingData.DataVector;
            classes = trainingData.Classes;
        }

        /// <summary>
        /// Specifies the attributes that should be serialized.
        /// </summary>
        /// <param name="info">the data necessary to deserialize this object</param>
        /// <param name="ctxt">specifies the serialization stream</param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("name", name);
            info.AddValue("category", category);
            info.AddValue("dataVector", dataVector);
            info.AddValue("classes", classes);
        }

        #endregion
    }
}
